feat(matrix): support (array), (string) and (bool) casts via engine handlers - #17
Merged
Merged
Conversation
This was referenced Aug 7, 2026
Owner
Author
|
Rebase and don’t wait for 8.4.1 tag - use what is inside master branch of native-matrix now - multiple versions of z-engine and wide php:^8.4 (this should come from rebase) |
…andlers Implements ObjectCastInterface::__cast() dispatching on the cast type: pretty-printed rows for (string), true for (bool) — a valid matrix is never empty by construction — and toArray() for IS_ARRAY. Unsupported cast types fall through to $hook->proceed(); when the default engine handler reports failure (numeric casts write no value), the handler substitutes the engine's own documented fallback of 1/1.0 because the z-engine trampoline always reports success to the engine. The handler runs across the FFI boundary and therefore never throws. Two PHP 8.4 realities shape the implementation: - PHP 8.1 inserted IS_NEVER = 17 into the engine type table, shifting _IS_BOOL to 18 and _IS_NUMBER to 19; z-engine dev-master still declares the pre-8.1 values, so boolean casts are matched against a local ENGINE_IS_BOOL = 18 constant instead of the stale ReflectionValue::_IS_BOOL. - (array) casts do not reach cast_object at all: the engine routes them through get_properties_for with the ARRAY_CAST purpose. Matrix now also implements ObjectGetPropertiesForInterface::__getFields(), returning the rows for array casts while reproducing the default property table for debugging, serialization, var_export and JSON so their output stays byte-identical. get_object_vars() exposes only publicly visible entries because the engine skips visibility filtering once a custom handler is installed. CastObjectHook::getResult() is deliberately never called after a failed proceed(): the retval slot is uninitialized scratch memory in that case and reading it corrupts the calling VM frame. Closes #9 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
…item Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
…ies hooks Review follow-up: the silently-absent engine warning on numeric cast fallback, the scope-insensitive public view served to get_object_vars() callers, and the getResult()-only-after-success rule are all deliberate; say so where a future change would otherwise "fix" them. Also corrects the stale-constants note to reference the z-engine 8.4 line instead of dev-master — composer.json already pins the stable 8.4.0 tag — and imports ARRAY_FILTER_USE_KEY for consistency with the function imports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
The scope-insensitivity of the get_properties_for hook is a documented deviation from default engine behaviour; lock it in a test so a change there is a conscious decision, and name the intentionally absent engine warning in the numeric-fallback test title. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
…-through z-engine 8.4.1 (PRs lisachenko/z-engine#155 and #156) ships the API the local workarounds stood in for: CastType/PropertyPurpose enums exposed via getCastTypeEnum()/getPurposeEnum(), guarded upstream against the generated engine ground truth, and a CastObjectHook whose fall-through behaves exactly like an uninstalled handler. Drop the local ENGINE_IS_BOOL/ENGINE_IS_NUMBER/PROP_PURPOSE_* constants — values that can silently drift between PHP minors — and dispatch on the named cases instead. The numeric-cast fallback is gone entirely: __cast now defers to proceed()/getResult(), and the failed cast propagates to the engine caller, which emits its own "could not be converted to int/float" warning and substitutes 1 — the previously undeliverable default diagnostic is restored, and the fallback test asserts it. Raises the z-engine floor to ~8.4.1 accordingly, in lockstep with the PHP 8.4 pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
lisachenko
force-pushed
the
claude/casting-operator-jukdna
branch
from
August 7, 2026 22:32
368684c to
8896331
Compare
Owner
Author
|
Done — rebased onto master, so the branch now carries Generated by Claude Code |
lisachenko
commented
Aug 7, 2026
With the suite running on PHP 8.4 and 8.5 in parallel, a version-gated skip could silently shrink coverage on one leg while the job stays green. failOnSkipped/failOnIncomplete in phpunit.xml.dist turn any skipped or incomplete test into a failure for both CI legs and local runs alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
Review follow-up: the cast fall-through docblock still promised the behaviour "with z-engine >= 8.4.1", but the package consumes the 8.4.x-dev/8.5.x-dev branches now - no tagged version to point at. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #9.
What this does
MatriximplementsObjectCastInterface::__cast()andObjectGetPropertiesForInterface::__getFields(), dispatched by the engine'scast_object/get_properties_forhandlers:(string) $matrix→ pretty-printed rows, one per line:[1, 2, 3](bool) $matrix→true(a valid matrix is never empty by construction)(array) $matrix→toArray()— array casts arrive atget_properties_forwith theArrayCastpurpose, not atcast_object(theCastType::Arraybranch in__caststays for engine paths that passIS_ARRAYdirectly)Object of class ... could not be converted to int/floatplus the substitute value1/1.0, exactly as with no handler installedget_properties_forpurpose (debug, serialize, var_export, JSON) reproduces the default property table viaget_mangled_object_vars(), sovar_dump()/serialize()/var_export()/json_encode()output is byte-identical to before (covered by tests)GET_OBJECT_VARSpurpose serves the publicly visible entries only — the engine skips visibility filtering entirely once a custom handler is installed, so without thisget_object_vars($m)would leak the mangled private table. Known deviation: class-scoped callers (a closure bound toMatrix) also get the public view, since the calling scope is not recoverable inside the FFI callback; documented and pinned by its own.phpt.Neither hook ever throws — they run inside FFI callbacks, where PHP escalates any exception into a fatal engine error.
All dispatch is on z-engine's named
CastType/PropertyPurposeenums (getCastTypeEnum()/getPurposeEnum()), not numeric constants — the values are guarded upstream against the generated engine ground truth and adapt per z-engine line, which is what lets the same code run on 8.4 and 8.5.Upstream work this PR surfaced (both merged)
Implementing this uncovered two z-engine bugs, fixed on the
8.4branch and merged up tomaster:ReflectionValue::_IS_BOOL/_IS_NUMBERpredated PHP 8.1'sIS_NEVERinsertion, so boolean casts (engine id 18) misrouted into_IS_NUMBERbranches. The fix also added theCastType/PropertyPurposeenums this PR dispatches on.CastObjectHookcould not fall through safely:getResult()after a failedproceed()read the uninitialized retval slot and corrupted VM frames, and the trampoline's unconditionalSUCCESSmade the engine's default warning+substitute behaviour unreachable. With the fix,__cast's fall-through is the naiveproceed(); return getResult();and behaves exactly like an uninstalled handler.Earlier revisions of this branch carried local workarounds for both (hand-declared
ENGINE_IS_BOOL/PROP_PURPOSE_*constants, a hand-rolled substitute-value fallback); the final revision drops them for the upstream API.Rebased onto multi-version master
Per review feedback, the branch is rebased onto the
php: ^8.4+z-engine: 8.4.x-dev || 8.5.x-devmaster — no tag dependency. Verified with fresh installs on both minors:8.4.x-dev): 19/19 tests, PHPStan level max clean, PER-CS2.0 cleandev-master): 19/19 tests, PHPStan level max cleanTests
Seven new
.phptfiles (all with the mandatory three-line--INI--section): array/string/bool casts, the numeric fall-through (asserting the restored engine warning via--EXPECTREGEX--), plus regression locks forvar_dump()output andget_object_vars()visibility (outside and class-scoped).🤖 Generated with Claude Code
https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En